/**
 * Copyright (C) 2001-2020 by RapidMiner and the contributors
 * 
 * Complete list of developers available at our web site:
 * 
 * http://rapidminer.com
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import javax.swing.BorderFactory;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkEvent.EventType;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.rapidminer.Process;
import com.rapidminer.gui.actions.SaveAction;
import com.rapidminer.gui.look.Colors;
import com.rapidminer.gui.processeditor.ProcessEditor;
import com.rapidminer.gui.tools.ExtendedJScrollPane;
import com.rapidminer.gui.tools.FilterTextField;
import com.rapidminer.gui.tools.ResourceDockKey;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.UpdateQueue;
import com.rapidminer.gui.tools.components.FeedbackForm;
import com.rapidminer.gui.tools.dialogs.ConfirmDialog;
import com.rapidminer.io.process.ProcessOriginProcessXMLFilter;
import com.rapidminer.io.process.XMLTools;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorCreationException;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.OperatorService;
import com.rapidminer.tools.WebServiceTools;
import com.rapidminer.tools.XMLException;
import com.rapidminer.tools.plugin.Plugin;
import com.vlsolutions.swing.docking.DockKey;
import com.vlsolutions.swing.docking.Dockable;


/**
 *
 * This class contains methods that generate an item that shows a help text either from an XML file
 * if provided or from the description contained by the operator itself. The actual document is
 * generated by the {@link OperatorDocToHtmlConverter}.
 *
 * @author Philipp Kersting, Marco Boeck
 *
 */
public class OperatorDocumentationBrowser extends JPanel implements Dockable, ProcessEditor {

	private static final long serialVersionUID = 1L;

	private static final String DOCUMENTATION_ROOT = "core/";
	private static final Dimension MINIMUM_DOCUMENTATION_SIZE = new Dimension(100, 100);
	private static final String FEEDBACK_KEY_DOCUMENTATION = "operator_documentation";

	public static final String OPERATOR_HELP_DOCK_KEY = "operator_help";

	private JEditorPane editor;

	private Operator displayedOperator = null;

	private boolean ignoreSelections = false;

	private final DockKey DOCK_KEY = new ResourceDockKey(OPERATOR_HELP_DOCK_KEY);

	private UpdateQueue documentationUpdateQueue = new UpdateQueue("documentation_update_queue");

	private GridBagConstraints contentGbc;
	private JPanel contentPanel;
	private FeedbackForm feedbackForm;


	/**
	 * Prepares the dockable and its elements.
	 */
	public OperatorDocumentationBrowser() {
		setLayout(new BorderLayout());

		contentGbc = new GridBagConstraints();

		contentPanel = new JPanel(new GridBagLayout()) {

			@Override
			public Dimension getPreferredSize() {
				return new Dimension(getParent().getWidth(), super.getPreferredSize().height);
			}
		};

		editor = new JEditorPane("text/html", "<html>-</html>") {

			@Override
			public Dimension getPreferredSize() {
				return new Dimension(getParent().getWidth(), super.getPreferredSize().height);
			}
		};

		// Instantiate Editor and set Settings
		editor.addHyperlinkListener(new OperatorHelpLinkListener());
		editor.setEditable(false);
		HTMLEditorKit hed = new HTMLEditorKit();
		hed.setStyleSheet(createStyleSheet(hed.getStyleSheet()));
		editor.setEditorKit(hed);
		editor.setBackground(Colors.PANEL_BACKGROUND);
		editor.setContentType("text/html");

		// add editor to scrollPane
		JScrollPane scrollPane = new ExtendedJScrollPane(contentPanel);
		scrollPane.setBorder(null);
		scrollPane.setMinimumSize(MINIMUM_DOCUMENTATION_SIZE);
		scrollPane.setPreferredSize(MINIMUM_DOCUMENTATION_SIZE);

		contentGbc.gridx = 0;
		contentGbc.gridy = 0;
		contentGbc.weightx = 1.0f;
		contentGbc.fill = GridBagConstraints.HORIZONTAL;
		contentPanel.add(editor, contentGbc);

		// add filler at bottom
		contentGbc.gridy += 1;
		contentGbc.weighty = 1.0f;
		contentGbc.fill = GridBagConstraints.BOTH;
		contentPanel.add(new JLabel(), contentGbc);

		// prepare contentGbc for feedback form
		contentGbc.gridy += 1;
		contentGbc.weighty = 0.0f;
		contentGbc.fill = GridBagConstraints.HORIZONTAL;

		// add scrollPane to Dockable
		this.add(scrollPane, BorderLayout.CENTER);

		this.setVisible(true);
		this.validate();

		documentationUpdateQueue.start();
	}

	@Override
	public void processChanged(Process process) {
		// not needed
	}

	/**
	 * This method gets called if the user clicks on an operator that has been placed in the
	 * process.
	 */
	@Override
	public void setSelection(List<Operator> selection) {
		if (selection != null && !selection.isEmpty()) {
			Operator operator = selection.get(0);
			if (!operator.equals(displayedOperator) && !ignoreSelections) {
				assignDocumentation(operator);
			}
		}
	}

	/**
	 * This is called by the {@link #setSelection(List)} method. It creates an absolute path that
	 * indicates the corresponding documentation XML file.
	 */
	private void assignDocumentation(Operator operator) {
		changeDocumentation(operator);
		displayedOperator = operator;
	}

	@Override
	public void processUpdated(Process process) {
		// not needed
	}

	@Override
	public Component getComponent() {
		return this;
	}

	@Override
	public DockKey getDockKey() {
		return DOCK_KEY;
	}

	/**
	 * Event handler that handles clicking on a link to a tutorial process or internal anchor
	 */
	private class OperatorHelpLinkListener implements HyperlinkListener {

		@Override
		public void hyperlinkUpdate(HyperlinkEvent e) {
			if (e.getEventType().equals(EventType.ACTIVATED)) {
				if (e.getDescription().startsWith("tutorial:")) {
					int index = Integer.parseInt(e.getDescription().substring("tutorial:".length())) - 1;
					try {
						ignoreSelections = true;
						openTutorialProcess(displayedOperator.getOperatorDescription().getKey(), index);
					} finally {
						ignoreSelections = false;
					}

				} else if (e.getDescription().startsWith("#")) {
					// go to internal anchor
					String desc = e.getDescription();
					desc = desc.substring(1);
					editor.scrollToReference(desc);
				} else if (e.getDescription().startsWith("tag:")) {
					// filter tag in operator list
					FilterTextField filterField = RapidMinerGUI.getMainFrame().getNewOperatorEditor().getNewOperatorGroupTree().getFilterField();
					filterField.setFilterText(e.getDescription().substring(4));
				} else {
					// open url in default browser
					Desktop desktop = Desktop.getDesktop();
					if (desktop.isSupported(Desktop.Action.BROWSE)) {
						URI uri;
						try {
							uri = new java.net.URI(e.getDescription());
							desktop.browse(uri);
						} catch (URISyntaxException e1) {
							LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.desktop.browse.malformed_url", e1);
						} catch (IOException e1) {
							LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.desktop.browse.open_browser", e1);
						}
					} else {
						LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.desktop.browse.not_supported");
					}
				}
			}
		}
	}

	/**
	 * Refreshes the documentation text.
	 *
	 * @param operator
	 *           the operator for which to load the documentation
	 */
	private void changeDocumentation(final Operator operator) {
		documentationUpdateQueue.execute(() -> {

			final String finalHtml = OperatorDocLoader.getDocumentation(operator);
			SwingUtilities.invokeLater(() -> {
				editor.setText("<html>" + finalHtml + "</html>");
				editor.setCaretPosition(0);

				if (feedbackForm != null) {
					contentPanel.remove(feedbackForm);
				}
				feedbackForm = new FeedbackForm(FEEDBACK_KEY_DOCUMENTATION, operator.getOperatorDescription().getKey());
				feedbackForm.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Colors.TAB_BORDER));
				contentPanel.add(feedbackForm, contentGbc);
			});

		});
	}

	/**
	 * This method creates and returns a stylesheet that makes the documentation look as it's
	 * supposed to look.
	 *
	 * @return the stylesheet
	 */
	private StyleSheet createStyleSheet(StyleSheet css) {
		css.addRule("body {font-family: Open Sans; font-size: 10px;}");
		css.addRule("p {font-size:10px; font-family: Open Sans; margin-top: 0px; padding-top: 0px;}");
		css.addRule("ul li {padding-bottom:1ex; font-family: Open Sans; font-size:10px; list-style-type: circle;}");
		css.addRule("h2 {font-size:14px; font-family: Open Sans; margin-bottom: 0px; margin-top: 0px;}");
		css.addRule("h4 {color: #000000; font-size:10px; font-family: Open Sans; font-weight: bold; margin-bottom: 5px;}");
		css.addRule("h5 {color: #3399FF; font-size:11px; font-family: Open Sans;}");
		css.addRule("h5 img {margin-right:8px; font-family: Open Sans;}");
		css.addRule(
				".parametersHeading {color: #000000; font-size:10px; font-family: Open Sans; font-weight: bold; margin-bottom: 0px;}");
		css.addRule(".parametersTable {cellspacing: 0px; border: 0;}");
		css.addRule(".typeIcon {height: 10px; width: 10px;}");
		css.addRule("td {vertical-align: top; font-family: Open Sans;}");
		css.addRule(".lilIcon {padding: 2px 4px 2px 0px;}");
		css.addRule("td {font-size: 10px; font-family: Open Sans;}");
		css.addRule(".packageName {color: #777777; font-size:10px; font-family: Open Sans; font-weight: normal;}");
		css.addRule(".parameterDetails {color: #777777; font-size:9px; font-family: Open Sans;}");
		css.addRule(".parameterDetailsCell{margin-bottom: 4px; padding-bottom: 4px;}");
		css.addRule(".tutorialProcessLink {margin-top: 6px; margin-bottom: 5px;}");
		css.addRule("hr {border: 0;height: 1px;}");
		css.addRule("a {color:" + SwingTools.getColorHexValue(Colors.LINKBUTTON_LOCAL) + "}");
		css.addRule("table {align:left;}");
		css.addRule(".tags {font-size: 9px; color: #777777;}");
		return css;
	}

	/**
	 * Sets the operator for which the operator documentation is shown.
	 *
	 * @param operator
	 * 		the operator for which to show the documentation
	 */
	public void setDisplayedOperator(Operator operator) {
		if (operator != null && !operator.getOperatorDescription().isDeprecated() && (this.displayedOperator == null || !operator.getOperatorDescription()
				.getKey().equals(this.displayedOperator.getOperatorDescription().getKey()))) {
			assignDocumentation(operator);
		}
	}

	/**
	 * Returns the resource path to the operator_name.xml of the given operator
	 *
	 * @param op the operator
	 * @return the resource path
	 */
	public static URL getDocResourcePath(Operator op) {
		Plugin provider = op.getOperatorDescription().getProvider();
		boolean isExtension = provider != null;
		String documentationRoot = isExtension ? provider.getPrefix() + "/" : DOCUMENTATION_ROOT;
		String groupPath = op.getOperatorDescription().getGroup().replace(".", "/");

		// if extension uses the extension folder as tree root...
		if (isExtension && provider.useExtensionTreeRoot()
				&& groupPath.startsWith(OperatorDescription.EXTENSIONS_GROUP_IDENTIFIER)) {

			// remove extension group identifier
			groupPath = groupPath.substring(groupPath.indexOf('/') + 1);

			// remove extension name
			int firstIndexOfSlash = groupPath.indexOf('/');
			if (firstIndexOfSlash != -1) {
				groupPath = groupPath.substring(firstIndexOfSlash + 1) + "/";
			} else {
				groupPath = "";
			}
		} else {
			groupPath += "/";
		}
		String key = op.getOperatorDescription().getKeyWithoutPrefix();

		String opDescXMLResourcePath = documentationRoot + groupPath + key + ".xml";
		return Plugin.getMajorClassLoader().getResource(opDescXMLResourcePath);
	}

	/**
	 * Tries to open the given tutorial, if the tutorial process exists a confirm dialog is shown
	 *
	 * @param operatorKey
	 * 		the operator key
	 * @param tutorialIndex
	 * 		index of the tutorial
	 */
	public static void openTutorialProcess(String operatorKey, int tutorialIndex) {
		String[] tutorialProcesses = OperatorDocumentationBrowser.getTutorialProcesses(operatorKey);
		if (tutorialIndex < 0 || tutorialIndex >= tutorialProcesses.length) {
			LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.documentation.ExampleProcess.invalid_index", new Object[]{operatorKey, tutorialIndex});
			return;
		}

		Process exampleProcess;
		try {
			exampleProcess = new Process(tutorialProcesses[tutorialIndex]);
		} catch (IOException | XMLException e) {
			LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.documentation.ExampleProcess.reading_file_error", e);
			return;
		}

		ProcessOriginProcessXMLFilter.setProcessOriginState(exampleProcess, ProcessOriginProcessXMLFilter.ProcessOriginState.GENERATED_TUTORIAL);
		if (permissionToOpen()) {
			RapidMinerGUI.getMainFrame().setProcess(exampleProcess, true);
			Collection<Operator> displayedOperators = RapidMinerGUI.getMainFrame().getProcess()
					.getAllOperators();
			for (Operator item : displayedOperators) {
				if (operatorKey.equals(item.getOperatorDescription().getKey())) {
					RapidMinerGUI.getMainFrame().selectOperator(item);
					break;
				}
			}
		}
	}

	/**
	 * Returns the tutorial process xml for the given operator key
	 *
	 * @param operatorKey
	 * 		the operator key
	 * @return the tutorial processes
	 */
	private static String[] getTutorialProcesses(String operatorKey) {
		Operator operator;
		try {
			operator = OperatorService.createOperator(operatorKey);
		} catch (OperatorCreationException e) {
			LogService.log(LogService.getRoot(), Level.WARNING, e, "com.rapidminer.tools.documentation.ExampleProcess.operator_not_found", operatorKey);
			return new String[0];
		}
		URL currentResourceURL = OperatorDocumentationBrowser.getDocResourcePath(operator);
		Document document = null;
		try {
			document = XMLTools.parse(WebServiceTools.openStreamFromURL(currentResourceURL));
		} catch (SAXException | IOException e) {
			LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.documentation.ExampleProcess.parsing_xml_error", e);
			return new String[0];
		}

		NodeList nodeList = document.getElementsByTagName("tutorialProcess");
		if (nodeList.getLength() == 0) {
			// no tutorial process
			return new String[0];
		}
		Node process = null;
		List<String> processes = new ArrayList<>();
		for (int tutorialPos = 0; tutorialPos < nodeList.getLength(); tutorialPos++) {
			Node processNode = nodeList.item(tutorialPos);
			for (int i = 0; i < processNode.getChildNodes().getLength(); i++) {
				if ("process".equals(processNode.getChildNodes().item(i).getNodeName())) {
					process = processNode.getChildNodes().item(i);
					break;
				}
			}

			StringWriter buffer = new StringWriter();
			DOMSource processSource = new DOMSource(process);
			try {
				Transformer t = TransformerFactory.newInstance().newTransformer();
				t.transform(processSource, new StreamResult(buffer));
			} catch (TransformerException e) {
				LogService.getRoot().log(Level.WARNING,
						"com.rapidminer.tools.documentation.ExampleProcess.creating_example_process_error", e);
				return new String[0];
			}
			processes.add(buffer.toString());
		}

		return processes.toArray(new String[0]);

	}

	/**
	 * Shows a confirmation dialog if needed
	 *
	 * @return {@code true} if the user want's to open the tutorial process
	 */
	private static boolean permissionToOpen() {
		// ask for confirmation before stopping the currently running process and
		// opening another one!
		if ((RapidMinerGUI.getMainFrame().getProcessState() == Process.PROCESS_STATE_RUNNING
				|| RapidMinerGUI.getMainFrame().getProcessState() == Process.PROCESS_STATE_PAUSED)
				&& SwingTools.showConfirmDialog("close_running_process", ConfirmDialog.YES_NO_OPTION) != ConfirmDialog.YES_OPTION) {
			return false;
		}

		// ask user if he wants to save his current process because the example process
		// will replace his current process
		if (RapidMinerGUI.getMainFrame().isChanged()) {
			// current process is flagged as unsaved
			int returnVal = SwingTools.showConfirmDialog("save_before_show_tutorial_process",
					ConfirmDialog.YES_NO_CANCEL_OPTION);
			if (returnVal == ConfirmDialog.CANCEL_OPTION) {
				return false;
			} else if (returnVal == ConfirmDialog.YES_OPTION) {
				SaveAction.saveAsync(RapidMinerGUI.getMainFrame().getProcess());
			}
		} else {
			// current process is not flagged as unsaved
			return SwingTools.showConfirmDialog("show_tutorial_process",
					ConfirmDialog.OK_CANCEL_OPTION) != ConfirmDialog.CANCEL_OPTION;
		}
		return true;
	}
}

